package com.tiantian.novel.helper;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import com.tiantian.novel.model.ChapterInfo;
import com.tiantian.novel.model.GroupInfo;
import com.tiantian.novel.model.NovelSummary;
import com.tiantian.novel.resolver.NovelResolver;
import com.tiantian.novel.resolver.NovelResolverProxy;
import com.tiantian.novel.service.StatusManager;
import org.apache.commons.io.FileUtils;
import org.jsoup.HttpStatusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Novel utils
 *
 * @author wangkai
 * @since 2020/12/17.
 */
public class NovelHelper {
    private static final Logger log = LoggerFactory.getLogger(NovelHelper.class);
    private static final String STYLE_DEFAULT = "default";
    private static final String STYLE_KF8 = "kf8";

    private static final String KINDLE_HOME = "templates/kindle";
    /**
     * Novel Store Directory
     */
    private String novelHome;
    /**
     * 系统部署以后存放kindle相关文件的目录
     */
    private String kindleHome;
    private String style;
    private String commonHome;

    public NovelHelper(String kindleHome, String novelHome) {
        this(kindleHome, STYLE_DEFAULT, novelHome);
    }

    public NovelHelper(String kindleHome, String style, String novelHome) {
        this.kindleHome = kindleHome + File.separator + KINDLE_HOME;
        this.style = style;
        this.novelHome = novelHome;
        this.commonHome = style + "/common";
        log.info("_init kindleHome:【{}】style:【{}】novelHome:【{}】", kindleHome, style, novelHome);
        try {
            _init();
        } catch (IOException e) {
            log.error("", e);
        }
    }

    private void _init() throws IOException {
        try {
            log.info("_init novel kindleHome:【{}】novelHome:【{}】", kindleHome, novelHome);
            String jarFile = this.getClass().getProtectionDomain().getCodeSource().getLocation().getFile();
            JarHelper.getInstance(true).copyFolder(jarFile, KINDLE_HOME, kindleHome);
        } catch (Exception e) {
            log.error("初使化novel home失败:", e);
        }
        if (!isWindows()) {
            chmod();
        }
    }

    /**
     * 生成mobi
     *
     * @param novel
     *
     * @throws Exception
     */
    public void genMobi(NovelSummary novel) throws Exception {
        _genMobi(novel, novelHome);
    }

    /**
     * 生成mobi
     *
     * @param novel
     * @param store
     */
    private void _genMobi(NovelSummary novel, String store) throws Exception {
        log.info("生成mobi开始：【{}】", novel.getTitle());
        loadChapters(novel);
        String folder = store + File.separator + novel.getCode() + "_mobi";
        KindleFreemarkerHelper freemarker = KindleFreemarkerHelper.getInstance(kindleHome, style);
        _genSource(novel, folder, freemarker);
        _genMobiFile(novel.getTitle(), folder, freemarker);
        log.info("生成mobi完成：【{}】【{}】", novel.getTitle(), folder);
    }

    /**
     * 加载章节的内容
     *
     * @param novel NovelInfo
     */
    private void loadChapters(NovelSummary novel) throws IOException {
        Objects.requireNonNull(novel);
        List<ChapterInfo> chapters = novel.getChapters();
        log.info("load novel chapters start【{}】", novel.getTitle());
        chapters.stream()
                .forEach(item -> {
                    try {
                        _fetchFromLocal(novel.getCode(), item);
                    } catch (Exception e) {
                        log.error("_fetchFromLocal failed:", e);
                    }
                });

        log.info("load novel chapters finished【{}】", novel.getTitle());
    }

    /**
     * 基于my_ftl生成source
     *
     * @param novel
     * @param folder
     * @param freemarker
     */
    private void _genSource(NovelSummary novel, String folder, KindleFreemarkerHelper freemarker) throws Exception {
        if (novel == null || novel.getChapters() == null || novel.getChapters().size() == 0) {
            throw new Exception("chapters不能为空");
        }
        // create folder
        File dir = createFolder(folder, freemarker.getAbsolutePath(commonHome));
        _downloadLogo(novel, dir, freemarker);
        _genTOC(novel, dir, freemarker);
        _genText(novel, dir, freemarker);
        _genNCX(novel, dir, freemarker);
        _genOPF(novel, dir, freemarker);
        log.info("生成source files完成。");
    }

    /**
     * 下载logo
     *
     * @param novel
     * @param dir
     * @param freemarker
     */
    private void _downloadLogo(NovelSummary novel, File dir, KindleFreemarkerHelper freemarker) {
        if (novel.getLogo() == null || novel.getLogo().isEmpty()) {
            return;
        }
        File dest = new File(dir.getAbsolutePath() + File.separator + "images" + File.separator + "cover.jpg");
        log.info("生成logo开始:【{}】【{}】", novel.getTitle(), novel.getLogo());
        try {
            byte[] img = getResolver().getImage(novel.getLogo());
            FileUtils.writeByteArrayToFile(dest, img);
        } catch (IOException e) {
            log.error("下载图片失败", e);
        }
        log.info("生成logo完成:【{}】【{}】", novel.getTitle(), dest.getPath());
    }

    /**
     * 生成正文
     *
     * @param novel
     * @param dir
     * @param freemarker
     */
    private void _genText(NovelSummary novel, File dir, KindleFreemarkerHelper freemarker) throws Exception {
        log.info("生成TEXT开始:【{}】【{}】", novel.getTitle(), dir.getAbsolutePath());
        String ftl = "text.html";
        File dest = null;
        for (ChapterInfo chapter : novel.getChapters()) {
            log.info("生成html开始:【{}】", chapter.getTitle());
            dest = new File(dir.getAbsolutePath() + File.separator + "page" + chapter.getId() + ".html");
            freemarker.toFile(ftl, chapter, dest);
            log.info("生成html完成:【{}】【{}】", chapter.getTitle(), dest.getAbsolutePath());
        }
        log.info("生成TEXT完成:【{}】【{}】", novel.getTitle(), dir.getAbsolutePath());
    }

    /**
     * 生成mobi源文件
     *
     * @param title
     * @param author
     * @param filename
     * @param chapters
     * @param store
     *
     * @throws Exception
     */
    private void genSource4kf8(String title,
                               String author,
                               String filename,
                               String store,
                               List<ChapterInfo> chapters) throws Exception {
        // todo fixed
        if (chapters == null || chapters.size() == 0) {
            throw new Exception("参数chapters不能为空");
        }
        int chapterCount = chapters.size();
        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("title", title);
        dataModel.put("author", author);
        dataModel.put("chapters", chapters);
        KindleFreemarkerHelper freemarker = KindleFreemarkerHelper.getInstance(kindleHome, NovelHelper.STYLE_KF8);
        File dir = createFolder(store + File.separator + filename, freemarker.getAbsolutePath(commonHome));
        List<GroupInfo> groups = new ArrayList<GroupInfo>();
        List<ChapterInfo> gChapters = new ArrayList<>();
        // 100章为一个小节
        int groupIndex = 1;
        for (int i = 1; i <= chapterCount; i++) {
            gChapters.add(chapters.get(i - 1));
            if (i % 100 == 0) {
                groups.add(new GroupInfo(groupIndex, "group" + groupIndex, gChapters));
                groupIndex++;
                gChapters = new ArrayList<>();
            }
        }
        dataModel.put("groups", groups);
        _genText4kf8(groups, dir, freemarker);
        _genTOC(dataModel, dir, freemarker);
        _genNCX(dataModel, dir, freemarker);
        _genOPF(dataModel, dir, freemarker);
        log.info("生成source files完成。");
    }

    /**
     * 生成mobi文件
     *
     * @param filename
     * @param store
     * @param freemarker
     */
    private int _genMobiFile(String filename, String store, KindleFreemarkerHelper freemarker) throws Exception {
        String opf = store + File.separator + "sample.opf";
        int result = _doGenMobiFile(filename, opf, freemarker);
        log.info("生成mobi文件完成,结果：【{}】,file【】", result, store + File.separator + filename + ".mobi");
        return result;
    }

    /**
     * 生成 .mobi file
     *
     * @param filename
     * @param opf
     * @param freemarker
     *
     * @return
     */
    private int _doGenMobiFile(String filename, String opf, KindleFreemarkerHelper freemarker) throws IOException,
            InterruptedException {
        if (isWindows()) {
            return _genFile4Windows(filename, opf, freemarker);
        } else {
            return _genFile4linux(filename, opf, freemarker);
        }
    }

    /**
     * 是否windows OS
     *
     * @return
     */
    private boolean isWindows() {
        log.info("OS.name ={}", System.getProperty("os.name").toLowerCase());
        String os = System.getProperty("os.name").toLowerCase();
        // windows
        return os.indexOf("win") >= 0;
    }

    private int chmod() throws IOException {
        KindleFreemarkerHelper freemarker = KindleFreemarkerHelper.getInstance(kindleHome, style);
        String exe = null;
        try {
            exe = freemarker.getExe("linux");
            String command = String.format("chmod +x %s", exe);
            log.info("给Exe文件授予可执行权限,command={}", command);
            Process process = Runtime.getRuntime().exec(command);
            process.waitFor();
            return process.exitValue();
        } catch (Exception e) {
            log.error("给Exe文件授予可执行权限失败：", e);
        }
        return 1;
    }

    /**
     * Windows 系统
     *
     * @return
     */
    private int _genFile4Windows(String filename, String opf, KindleFreemarkerHelper freemarker) throws IOException,
            InterruptedException {
        String exe = freemarker.getExe(null);
        String command = String.format("cmd /c start %s %s -o %s", exe, opf, filename + ".mobi");
        log.info("Windows:开始生成mobi文件,command={}", command);
        return _exec(exe, filename, opf);
    }

    /**
     * linux 系统
     *
     * @return
     */
    private int _genFile4linux(String filename, String opf, KindleFreemarkerHelper freemarker) throws IOException,
            InterruptedException {
        String exe = freemarker.getExe("linux");
        String command = String.format("%s %s -o %s", exe, opf, filename + ".mobi");
        log.info("Linux:开始生成mobi文件,command={}", command);
        return _exec(exe, filename, opf);
    }

    /**
     * 执行
     *
     * @param exe
     * @param filename
     * @param opf
     *
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    private int _exec(String exe, String filename, String opf) throws IOException, InterruptedException {
        String[] cmds = new String[]{exe, opf, "-o", filename + ".mobi"};
        log.info("exec Commands:{}", Arrays.toString(cmds));
        Process process = Runtime.getRuntime().exec(cmds);
        SequenceInputStream sis = new SequenceInputStream(process.getInputStream(), process.getErrorStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(sis, "UTF8"));
        String str;
        StringBuilder buf = new StringBuilder();
        while ((str = reader.readLine()) != null) {
            buf.append(str).append("\n");
            System.out.println(str);
        }
        reader.close();
        sis.close();
        process.waitFor();
        process.destroy();
        log.info("{}", buf.toString());
        return process.exitValue();
    }

    /**
     * 创建目录
     *
     * @param fullpath
     * @param commonDir
     *
     * @return
     */
    private File createFolder(String fullpath, String commonDir) throws Exception {
        File dir = new File(fullpath);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        // copyFolder common files
        FileUtils.copyDirectory(new File(commonDir), dir);
        return dir;
    }

    /**
     * 生成TOC
     *
     * @param dataModel
     * @param dir
     * @param freemarker
     */
    private void _genTOC(Object dataModel, File dir, KindleFreemarkerHelper freemarker) throws Exception {
        String ftl = "toc.html";
        File dest = new File(dir.getAbsolutePath() + File.separator + ftl);
        log.info("生成TOC开始:【{}】【{}】", ftl, dest.getPath());
        freemarker.toFile(ftl, dataModel, dest);
        log.info("生成TOC完成.");
    }

    /**
     * 生成NCX
     *
     * @param dataModel
     * @param dir
     * @param freemarker
     */
    private void _genNCX(Object dataModel, File dir, KindleFreemarkerHelper freemarker) throws Exception {
        String ftl = "toc.ncx";
        File dest = new File(dir.getAbsolutePath() + File.separator + ftl);
        log.info("生成NCX开始:【{}】【{}】", ftl, dest.getPath());
        freemarker.toFile(ftl, dataModel, dest);
        log.info("生成NCX完成.");
    }

    /**
     * 生成OPF
     *
     * @param dataModel
     * @param dir
     * @param freemarker
     */
    private void _genOPF(Object dataModel, File dir, KindleFreemarkerHelper freemarker) throws Exception {
        String ftl = "sample.opf";
        File dest = new File(dir.getAbsolutePath() + File.separator + ftl);
        log.info("生成OPF开始:【{}】【{}】", ftl, dest.getPath());
        freemarker.toFile(ftl, dataModel, dest);
        log.info("生成OPF完成.");
    }

    /**
     * 生成text
     *
     * @param groups
     * @param dir
     * @param freemarker
     */
    private void _genText4kf8(List<GroupInfo> groups, File dir, KindleFreemarkerHelper freemarker) throws Exception {
        String ftl = "text.html";
        for (GroupInfo group : groups) {
            File dest = new File(dir.getAbsolutePath() + File.separator + "page" + group.getId() + ".html");
            log.info("生成group HTML开始:【{}】", ftl);
            freemarker.toFile(ftl, group, dest);
            log.info("生成group TEXT完成:【{}】【{}】", ftl, dest.getPath());
        }
        log.info("生成TEXT完成.");
    }

    /**
     * 保存
     *
     * @param novel
     */
    public void store(NovelSummary novel) throws IOException {
        SimplePropertyPreFilter filter = new SimplePropertyPreFilter();
        filter.getExcludes().add("content");

        String json = JSONObject.toJSONString(novel, filter);
        if (log.isDebugEnabled()) {
            log.info("novelHome novel into file 【{}】", json);
        }
        String filename = novel.getCode();
        File file = new File(String.format("%s%s%s.novel", novelHome, File.separator, filename));
        if (!file.exists()) {
            file.getParentFile().mkdirs();
            file.createNewFile();
        }
        FileUtils.write(file, json);
        log.info("novelHome novel into file 【{}】", file.getAbsolutePath());
    }

    /**
     * 加novel info
     *
     * @param homePage
     *
     * @return
     */
    public NovelSummary loadCover(String homePage) throws Exception {
        Objects.requireNonNull(homePage);
        NovelSummary novel = getResolver().resolveNovelSummary(homePage);
        Objects.requireNonNull(novel);
        NovelSummary novel2 = loadCoverFromLocal(novel.getCode());
        if (null == novel2 || !novel.getUpdateTime().equals(novel2.getUpdateTime())) {
            store(novel);
            return novel;
        } else {
            return novel2;
        }
    }

    private NovelResolver getResolver() {
        return SpringUtil.getBean(NovelResolverProxy.class);
    }

    /**
     * query from local file(*.novel)
     *
     * @param code
     *
     * @return
     */
    public NovelSummary loadCoverFromLocal(String code) {
        File file = new File(String.format("%s%s%s.novel", novelHome, File.separator, code));
        if (!file.exists()) {
            return null;
        }
        try {
            String json = FileUtils.readFileToString(file);
            return JSONObject.parseObject(json, NovelSummary.class);
        } catch (IOException e) {
            log.error("loadCover local failed:", e);
            return null;
        }
    }

    /**
     * fetch all chapters
     *
     * @param novel
     *
     * @throws IOException
     */
    public void fetch(NovelSummary novel) throws IOException {
        _fetch(novel, true);
    }

    private Random random = new Random();

    /**
     * 生成一个随机数
     *
     * @return
     */
    private int getRandom() {
        return random.nextInt(3, 10);
    }

    /**
     * fetch chapters
     *
     * @param novel
     * @param save2file 是否save到file
     */
    private void _fetch(NovelSummary novel, boolean save2file) throws IOException {
        Objects.requireNonNull(novel);
        List<ChapterInfo> chapters = novel.getChapters();
        log.info("fetch novel start【{}】", novel.getTitle());
        // 创建存储目录
        File file = new File(String.format("%s%s%s", novelHome, File.separator, novel.getCode()));
        file.mkdirs();
        AtomicInteger count = new AtomicInteger(0);
        chapters.stream()
                .filter(chapterInfo -> !chapterInfo.isFinished())
                .forEach(item -> {
                    boolean fetchFromWeb = false;
                    try {
                        fetchFromWeb = _fetchFromWeb(novel.getCode(), item);
                    } catch (HttpStatusException se) {
                        log.error("_fetchFromWeb failed:{}", se.toString());
                    } catch (Exception e) {
                        log.error("_fetchFromWeb failed:", e);
                    }
                    // 标识当前从web获取了一次数据
                    if (fetchFromWeb) {
                        count.incrementAndGet();
                        // 每更新10章保存状态
                        if (count.get() % 10 == 0) {
                            try {
                                store(novel);
                                count.set(0);
                            } catch (IOException e) {
                                log.error("保存novel失败：", e);
                            }
                        }

                        long sleep = getRandom();
                        log.info("{}秒后读取下一章", sleep);
                        try {
                            TimeUnit.SECONDS.sleep(sleep);
                        } catch (InterruptedException e) {
                            log.error("InterruptedException：", e);
                        }
                    }

                });
        // 最后更新一波状态
        store(novel);
        log.info("fetch novel finished【{}】", novel.getTitle());
    }

    /**
     * fetch From Local
     *
     * @param code
     * @param item
     */
    private void _fetchFromLocal(String code, ChapterInfo item) throws IOException {
        File file = new File(
                String.format("%s%s%s%s%s.js", novelHome, File.separator, code, File.separator, item.getId()));
        if (file.exists()) {
            ChapterInfo tmp = JSONObject.parseObject(FileUtils.readFileToString(file), ChapterInfo.class);
            if (tmp == null) {
                log.error("chapter file is empty:{}", item.getUrl());
                file.delete();
                return;
            }
            item.setContent(tmp.getContent());
        } else {
            throw new RuntimeException("章节不存在," + item.getUrl());
        }
    }

    /**
     * fetch html content
     *
     * @param code
     * @param item
     */
    private boolean _fetchFromWeb(String code, ChapterInfo item) throws Exception {
        File file = new File(
                String.format("%s%s%s%s%s.js", novelHome, File.separator, code, File.separator, item.getId()));
        if (file.exists()) {
            item.setFinished(true);
            return false;
        } else {
            String taskId = code + "-" + item.getId();
            boolean lock = StatusManager.beginTask(taskId, item.getUrl());
            if (lock) {
                try {
                    ChapterInfo temp = getResolver().resovleChapter(item.getUrl());
                    Objects.requireNonNull(temp);
                    // fill content
                    item.setContent(temp.getContent());

                    SimplePropertyPreFilter filter = new SimplePropertyPreFilter();
                    filter.getExcludes().add("finished");

                    file.createNewFile();
                    FileUtils.write(file, JSONObject.toJSONString(item, filter));
                    log.info("Save Novel chapter into file:{}", file.getAbsolutePath());

                    item.setFinished(true);
                    return true;
                } catch (Exception e) {
                    log.error("解析章节内容失败:", e);
                } finally {
                    if (lock) {
                        StatusManager.finishTask(taskId);
                    }
                }
            }
            item.setFinished(false);
            return false;
        }
    }

    /**
     * 根据url加载本地全部chapters
     *
     * @param url
     *
     * @return
     * @throws IOException
     */
    public NovelSummary loadChapters(String url) throws Exception {
        NovelSummary novelSummary = loadCover(url);
        return loadChaptersByCode(novelSummary.getCode());
    }

    /**
     * 根据code加载本地全部chapters
     *
     * @param code
     *
     * @return
     * @throws IOException
     */
    private NovelSummary loadChaptersByCode(String code) throws IOException {
        NovelSummary novelSummary = loadCoverFromLocal(code);
        _fetch(novelSummary, true);
        return novelSummary;
    }

    /**
     * 获取mobi
     *
     * @param novel
     *
     * @return
     */
    public File getMobi(NovelSummary novel) throws Exception {
        File file = new File(
                novelHome + File.separator + novel.getCode() + "_mobi" + File.separator + novel.getTitle() + ".mobi");
        if (file.exists()) {
            return file;
        }
        genMobi(novel);
        file = new File(
                novelHome + File.separator + novel.getCode() + "_mobi" + File.separator + novel.getTitle() + ".mobi");
        return file;
    }
}
